/**
 * @ignore
 * dom-traversal
 * @author lifesinger@gmail.com, yiminghe@gmail.com
 */
var util = require('util');
var Dom = require('./api');
var NodeType = Dom.NodeType,
    CONTAIN_MASK = 16;

util.mix(Dom,
    /**
     * @override KISSY.DOM
     * @class
     * @singleton
     */
    {

        _contains: function (a, b) {
            return !!(a.compareDocumentPosition(b) & CONTAIN_MASK);
        },

        /**
         * Get the first element that matches the filter,
         * beginning at the first element of matched elements and progressing up through the Dom tree.
         * @param {HTMLElement[]|String|HTMLElement} selector Matched elements
         * @param {String|Function|String[]|Function[]} filter Selector string or filter function or array
         * @param {HTMLElement|String|HTMLDocument|HTMLElement[]} [context] Search bound element
         * @return {HTMLElement|HTMLElement[]}
         *  if filter is array, return all ancestors (include this) which match filter.
         *  else return closest parent (include this) which matches filter.
         */
        closest: function (selector, filter, context, allowTextNode) {
            return nth(selector, filter, 'parentNode', function (elem) {
                return elem.nodeType !== NodeType.DOCUMENT_FRAGMENT_NODE;
            }, context, true, allowTextNode);
        },

        /**
         * Get the parent of the first element in the current set of matched elements, optionally filtered by a selector.
         * @param {HTMLElement[]|String|HTMLElement} selector Matched elements
         * @param {String|Function|String[]|Function[]} [filter] Selector string or filter function or array
         * @param {HTMLElement|String|HTMLDocument|HTMLElement[]} [context] Search bound element
         * @return {HTMLElement|HTMLElement[]}
         *  if filter is array, return all ancestors which match filter.
         *  else return closest parent which matches filter.
         */
        parent: function (selector, filter, context) {
            return nth(selector, filter, 'parentNode', function (elem) {
                return elem.nodeType !== NodeType.DOCUMENT_FRAGMENT_NODE;
            }, context, undefined);
        },

        /**
         * Get the first child of the first element in the set of matched elements.
         * If a filter is provided, it retrieves the next child only if it matches that filter.
         * @param {HTMLElement[]|String|HTMLElement} selector Matched elements
         * @param {String|Function} [filter] Selector string or filter function
         * @return {HTMLElement}
         */
        first: function (selector, filter, allowTextNode) {
            var elem = Dom.get(selector);
            return nth(elem && elem.firstChild, filter, 'nextSibling',
                undefined, undefined, true, allowTextNode);
        },

        /**
         * Get the last child of the first element in the set of matched elements.
         * If a filter is provided, it retrieves the previous child only if it matches that filter.
         * @param {HTMLElement[]|String|HTMLElement} selector Matched elements
         * @param {String|Function} [filter] Selector string or filter function
         * @return {HTMLElement}
         */
        last: function (selector, filter, allowTextNode) {
            var elem = Dom.get(selector);
            return nth(elem && elem.lastChild, filter, 'previousSibling',
                undefined, undefined, true, allowTextNode);
        },

        /**
         * Get the immediately following sibling of the first element in the set of matched elements.
         * If a filter is provided, it retrieves the next child only if it matches that filter.
         * @param {HTMLElement[]|String|HTMLElement} selector Matched elements
         * @param {String|Function} [filter] Selector string or filter function
         * @return {HTMLElement}
         */
        next: function (selector, filter, allowTextNode) {
            return nth(selector, filter, 'nextSibling', undefined,
                undefined, undefined, allowTextNode);
        },

        /**
         * Get the immediately preceding  sibling of the first element in the set of matched elements.
         * If a filter is provided, it retrieves the previous child only if it matches that filter.
         * @param {HTMLElement[]|String|HTMLElement} selector Matched elements
         * @param {String|Function} [filter] Selector string or filter function
         * @return {HTMLElement}
         */
        prev: function (selector, filter, allowTextNode) {
            return nth(selector, filter, 'previousSibling',
                undefined, undefined, undefined, allowTextNode);
        },

        /**
         * Get the siblings of the first element in the set of matched elements, optionally filtered by a filter.
         * @param {HTMLElement[]|String|HTMLElement} selector Matched elements
         * @param {String|Function} [filter] Selector string or filter function
         * @return {HTMLElement[]}
         */
        siblings: function (selector, filter, allowTextNode) {
            return getSiblings(selector, filter, true, allowTextNode);
        },

        /**
         * Get the children of the first element in the set of matched elements, optionally filtered by a filter.
         * @param {HTMLElement[]|String|HTMLElement} selector Matched elements
         * @param {String|Function} [filter] Selector string or filter function
         * @return {HTMLElement[]}
         */
        children: function (selector, filter) {
            return getSiblings(selector, filter, undefined);
        },

        /**
         * Get the childNodes of the first element in the set of matched elements (includes text and comment nodes),
         * optionally filtered by a filter.
         * @param {HTMLElement[]|String|HTMLElement} selector Matched elements
         * @param {String|Function} [filter] Selector string or filter function
         * @return {HTMLElement[]}
         */
        contents: function (selector, filter) {
            return getSiblings(selector, filter, undefined, 1);
        },

        /**
         * Check to see if a Dom node is within another Dom node.
         * @param {HTMLElement|String} container The Dom element that may contain the other element.
         * @param {HTMLElement|String} contained The Dom element that may be contained by the other element.
         * @return {Boolean}
         */
        contains: function (container, contained) {
            container = Dom.get(container);
            contained = Dom.get(contained);
            if (container && contained) {
                return Dom._contains(container, contained);
            }
            return false;
        },
        /**
         * search for a given element from among the matched elements.
         * @param {HTMLElement|String} selector elements or selector string to find matched elements.
         * @param {HTMLElement|String} s2 elements or selector string to find matched elements.
         */
        index: function (selector, s2) {
            var els = Dom.query(selector),
                c,
                n = 0,
                p,
                els2,
                el = els[0];

            if (!s2) {
                p = el && el.parentNode;
                if (!p) {
                    return -1;
                }
                c = el;
                while ((c = c.previousSibling)) {
                    if (c.nodeType === NodeType.ELEMENT_NODE) {
                        n++;
                    }
                }
                return n;
            }

            els2 = Dom.query(s2);

            if (typeof s2 === 'string') {
                return util.indexOf(el, els2);
            }

            return util.indexOf(els2[0], els);
        },

        /**
         * Check to see if a Dom node is equal with another Dom node.
         * @param {HTMLElement|String} n1
         * @param {HTMLElement|String} n2
         * @return {Boolean}
         * @member KISSY.DOM
         */
        equals: function (n1, n2) {
            n1 = Dom.query(n1);
            n2 = Dom.query(n2);
            if (n1.length !== n2.length) {
                return false;
            }
            for (var i = n1.length; i >= 0; i--) {
                if (n1[i] !== n2[i]) {
                    return false;
                }
            }
            return true;
        }
    });

// 获取元素 elem 在 direction 方向上满足 filter 的第一个元素
// filter 可为 number, selector, fn array ，为数组时返回多个
// direction 可为 parentNode, nextSibling, previousSibling
// context : 到某个阶段不再查找直接返回
function nth(elem, filter, direction, extraFilter, context, includeSef, allowTextNode) {
    if (!(elem = Dom.get(elem))) {
        return null;
    }
    if (filter === 0) {
        return elem;
    }
    if (!includeSef) {
        elem = elem[direction];
    }
    if (!elem) {
        return null;
    }
    context = (context && Dom.get(context)) || null;

    if (filter === undefined) {
        // 默认取 1
        filter = 1;
    }
    var ret = [],
        isArray = util.isArray(filter),
        fi,
        filterLength;

    if (typeof filter === 'number') {
        fi = 0;
        filterLength = filter;
        filter = function () {
            return ++fi === filterLength;
        };
    }

    // 概念统一，都是 context 上下文，只过滤子孙节点，自己不管
    while (elem && elem !== context) {
        if ((
            elem.nodeType === NodeType.ELEMENT_NODE ||
            elem.nodeType === NodeType.TEXT_NODE && allowTextNode
            ) &&
            testFilter(elem, filter) &&
            (!extraFilter || extraFilter(elem))) {
            ret.push(elem);
            if (!isArray) {
                break;
            }
        }
        elem = elem[direction];
    }

    return isArray ? ret : ret[0] || null;
}

function testFilter(elem, filter) {
    if (!filter) {
        return true;
    }
    if (util.isArray(filter)) {
        var i, l = filter.length;
        if (!l) {
            return true;
        }
        for (i = 0; i < l; i++) {
            if (Dom.test(elem, filter[i])) {
                return true;
            }
        }
    } else if (Dom.test(elem, filter)) {
        return true;
    }
    return false;
}

// 获取元素 elem 的 siblings, 不包括自身
function getSiblings(selector, filter, parent, allowText) {
    var ret = [],
        tmp,
        i,
        el,
        elem = Dom.get(selector),
        parentNode = elem;

    if (elem && parent) {
        parentNode = elem.parentNode;
    }

    if (parentNode) {
        tmp = util.makeArray(parentNode.childNodes);
        for (i = 0; i < tmp.length; i++) {
            el = tmp[i];
            if (!allowText && el.nodeType !== NodeType.ELEMENT_NODE) {
                continue;
            }
            if (el === elem) {
                continue;
            }
            ret.push(el);
        }
        if (filter) {
            ret = Dom.filter(ret, filter);
        }
    }

    return ret;
}
/*
 2012-04-05 yiminghe@gmail.com
 - 增加 contents 方法

 2011-08 yiminghe@gmail.com
 - 添加 closest , first ,last 完全摆脱原生属性

 NOTES:
 - jquery does not return null ,it only returns empty array , but kissy does.

 - api 的设计上，没有跟随 jQuery. 一是为了和其他 api 一致，保持 first-all 原则。二是
 遵循 8/2 原则，用尽可能少的代码满足用户最常用的功能。
 */
